"""
Provides class:`Options`, to assist with command line option parsing and
printing of help messages. Developed to be independent of core Python library
modules: :mod:`argparse` and :mod:`optparse`, as the former was only
introduced to Python recently, while the latter was deprecated.
"""

import os
import re
import sys

class Options(object):
    """Class to parse command line options. Overwrite the class attributes to
    customise.
    """
    options = { '-in' : [],
                '-out': None }
    #: A dictionary containing keys and default arguments for all options.
    help_text = { '-in' : 'Input sequence file. Required',
                  '-out': 'Suffix of all the output files. By default, use ' + \
                          '-in, appending something to the suffix.' }
    #: A dictionary with same keys as :data:`.options`, containing help text
    #: for all options.
    switches = {}
    #: A dictionary containing help text for all options not requiring commands.
    #: All switches start with two dashes '--', and when provided will be
    #: changed to have a value of True.
    useage = None
    #: Show how the program is used (in programmer lingo).
    example = None
    #: Give an example of how to use it.
    singleargs = []
    #: List containing arguments that can take only one value.
    multiargs = []
    #: List containing the arguments that can take multiple values (e.g. can
    #: often provide multiple input files).
    regargs = [re.compile(r'^-group(\d+)')]
    #: for internal use.
    regopts = {}
    #: List of regular expressions for creating a dynamic number of groups.
    #: By default, groups are defined by -groupN where N is any positive
    #: integer. Subsequent arguments are file names that are coverged with -in
    #: when parse_args() is called.
    post_checks = []
    #: List of functions which can check the defined arguments after parsing the
    #: arguments.

    def __init__(self, args=None):
        super(Options, self).__init__()
        if sys.platform == 'win32':
            self.post_checks.append(self.expand_stars)
        if args is not None:
            self.parse_args(args)

    def print_help(self):
        """Prints help info for the options defined in self.options"""
        sys.stderr.write('\nValid options (take argument(s)):-\n')
        for key, val in self.help_text.items():
            sys.stderr.write('{0} : {1}\n'.format(key.ljust(15), val))
        if len(self.switches) > 0:
            sys.stderr.write('\nSwitches (take no arguments):-\n')
            for key, val in self.switches.items():
                sys.stderr.write('{0}: {1}\n'.format(key.ljust(15), val))
        if self.useage is not None:
            sys.stderr.write('\nUseage:-\n  ')
            sys.stderr.write(self.useage + '\n')
        if self.example is not None:
            sys.stderr.write('\nExample:-\n  ')
            sys.stderr.write(self.example.rstrip() + '\n')

    def __getitem__(self, key):
        return self.options[key]

    def __setitem__(self, key, value):
        if key in self.singleargs:
            self.options.update( { key : value } )
        elif key in self.multiargs:
            if type( self.options[key] ) == list:
                if type( value ) == list:
                    self.options[key] += value
                else:
                    self.options[key].append(value)
            else:
                self.options.update( { key : value } )
        else:
            raise KeyError('{0} is not defined as a singlearg or a multiarg!'
                           .format(key))

    def __repr__( self ):
        x = ''
        for i in self.options.iteritems():
            x += '{0}:{1}\n'.format(i[0].ljust(20), str(i[1]).rjust(20))
        if len( self.regopts ) > 0:
            x += '\nGroup arguments:\n'
            for i in self.regopts.iteritems():
                x += '{0}:{1}\n'.format(i[0].ljust(20), str(i[1]).rjust(20))
        return x

    def parse_args(self, args):
        """Give the command line options and this will update self.options
        and return those updated options as a dictionary."""
        if len(args) == 0:
            self.print_help()
            exit()
        prevargDash = False
        for i, arg in enumerate(args):
            if arg.startswith('--'):
                if arg in self.options:
                    self.options[ arg ] = True
                else:
                    sys.stderr.write('Invalid option: {0}\n'.format(args[i]))
                    self.print_help()
                    exit()
            elif arg.startswith('-'):
                matched = False
                if arg in self.options:
                    prevargDash = True
                    command = arg
                else:
                    for regarg in self.regargs:
                        match = regarg.search(arg)
                        if match:
                            prevargDash = True
                            matched = True
                            command = arg
                    if not matched:
                        sys.stderr.write('Invalid option: {0}'.format(args[i]))
                        self.print_help()
                        exit()
            elif prevargDash:
                if command in self.multiargs:
                    # These options can take multiple args.
                    if type( self.options[command] ) != list:
                        self.options.update( { command : [args[i]] } )
                    else:
                        self.options[command].append(args[i])
                elif command in self.singleargs:
                    # These options take a single argument.
                    self.options.update( { command : arg } )
                    prevargDash = False
                elif matched:
                    if command in self.regopts:
                        self.regopts[command].append( arg )
                    else:
                        self.regopts.update( { command : [ arg] } )
                else:
                    sys.stderr.write('Unrecognised command: {0}\n'
                                     .format(command))
            else:
                if '-in' in self.multiargs:
                    self.options['-in'].append(arg)
                else:
                    self.options.update({ '-in' : arg })
        if len( self.regopts ) > 0:
            self.options['_groups'] = []
            self.post_checks.append(self.check_groups)
        for meth in self.post_checks:
            meth()

        return self.options

    def check_groups(self):
        """Called automatically if any regular expression arguments are defined
        and found when parsing command line options.

        This checks to make sure that there are the same number of grouped files
        as there are input files. If they differ, then this will overwrite files
        provided to -in with files provided in the groups.
        """
        reg_files = self.regopts.values()   # list of lists
        self.options['_groups'] = [len(val) for val in reg_files]
        n_reg_files = sum(self.options['_groups'])
        err = sys.stderr.write
        if len(self.options['-in']) < n_reg_files:
            if len(self.options['-in']) > 0:
                err('Number of group files defined differs from the number of input files.\n')
                err('Taking files defined in groups as -in files.\n')
                self.options['-in'] = []
            for group in reg_files:
                for reg_file in group:
                    if reg_file not in self.options['-in']:
                        self.options['-in'].append( reg_file )
                    else:
                        err('{0} defined in -groups at least once already'
                            .format(reg_file))
        else:
            return

    def _expand_loop(self, path_arg):
        """On windows based machines, there is no shell expansion of
        stars. This sorts that out!"""
        dirs = self.path_reg.split(path_arg)
        matched = False
        for i, d in enumerate(dirs):
            if '*' in d:
                pwd = os.path.sep.join(dirs[:i])
                files = d.split('*')
                new_reg = re.compile(re.escape(files[0]) +
                                     r'.*' + re.escape(files[1]))
                for f_name in os.listdir(pwd):
                    match = new_reg.search(f_name)
                    if match:
                        matched = True
                        yield os.path.join(pwd, match.group())
        if not matched:
            yield path_arg

    def expand_stars( self ):
        # reg to allow use of cifs shares.
        self.path_reg = re.compile('(?<=[\w\d\s_\-])[\\/]{1}')
        iters = [self.regopts.items(), self.options.items()]
        for i, iterator in enumerate(iters):
            for key, value in iterator:
                if not hasattr(value, '__iter__'):
                    continue
                if key in self.singleargs:
                    try:
                        if not isinstance(value, str) and \
                           not isinstance(value, int):
                        #if type(value) is not (str or int):
                            continue
                        else:
                            value = value[0]
                        matches = [match for match in self._expand_loop(value)]
                    except Exception:
                        print(key)
                        raise
                    if len(matches) > 1:
                        raise LookupError("Only one value allowed for {0}. " +
                                          "Got more than one match".format(key))
                    elif len(matches) == 0:
                        continue
                    else:
                        if i == 0:
                            self.regopts[key] = matches[0]
                        self.options[key] = matches[0]
                else:
                    matches = []
                    for val in value:
                        matches += [match for match in self._expand_loop(val)]
                    if i == 0:
                        self.regopts[key] = matches
                    self.options[key] = matches
